Archivos

Por lo general, cuando se trabaja con un archivo se hacen tres operaciones seguidas:

  1. Abrir el archivo
  2. Procesar el archivo
  3. Cerrar el archivo

Y hay que tener cuidado, porque si ocurre algún error con el archivo en algún punto de su procesamiento es necesario encargarse de cerrarlo, antes de que la excepción siga subiendo niveles.

Trabajando con archivo de una forma segura

Para trabajar con los archivos de una forma más simple es que se agregó la sentencia with que define un contexto dentro del cual nos asegura que, ocurra una excepción o no, el archivo se cerrará al momento de salir de ese contexto:


In [1]:
try:
    with open('ejemplo.txt') as fd:
        print '¿El archivo se encuentra cerrado?', fd.closed
        a += 2  # Como la variable a no existe, va a tirar
                # una excepción del tipo NameError
        print 'Estas líneas nunca se van a mostrar porque'
        print 'antes va a ocurrir un error'
except NameError:
    print 'Ocurrio un error'
    
print '¿El archivo se encuentra cerrado?', fd.closed


¿El archivo se encuentra cerrado? False
Ocurrio un error
¿El archivo se encuentra cerrado? True

In [2]:
with open('ejemplo.txt', 'r') as archivo:
    print '¿El archivo se encuentra cerrado?: {}'.format(archivo.closed)
    print
    for linea in archivo:
        longitud = len(linea[:-1])
        print '{:2}: {}'.format(longitud, linea[:-1])

print
print '¿El archivo se encuentra cerrado?: {}'.format(archivo.closed)


¿El archivo se encuentra cerrado?: False

70: Python was created in the early 1990s by Guido van Rossum at Stichting
68: Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
64: as a successor of a language called ABC.  Guido remains Python's
70: principal author, although it includes many contributions from others.
 0: 
66: In 1995, Guido continued his work on Python at the Corporation for
70: National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
61: in Reston, Virginia where he released several versions of the
 9: software.
 0: 
64: In May 2000, Guido and the Python core development team moved to
70: BeOpen.com to form the BeOpen PythonLabs team.  In October of the same
62: year, the PythonLabs team moved to Digital Creations (now Zope
68: Corporation, see http://www.zope.com).  In 2001, the Python Software
62: Foundation (PSF, see http://www.python.org/psf/) was formed, a
66: non-profit organization created specifically to own Python-related
66: Intellectual Property.  Zope Corporation is a sponsoring member of
 8: the PSF.
 0: 
70: All Python releases are Open Source (see http://www.opensource.org for
69: the Open Source Definition).  Historically, most, but not all, Python
39: releases have also been GPL-compatible.

¿El archivo se encuentra cerrado?: True

Si bien no cerramos explícitamente el archivo usando la función close, al salir del bloque de código que encierra el with el archivo se encontrará cerrado.

Pickles

Los pickles son una forma de guardar estructuras de datos complejas y recuperarlas fácilmente, sin necesidad de convertirlas a texto y luego parsearlas:

Ejemplo 1: Guardar de a un elemento

Se puede usar los pickles como se hacía con los viejos archivos de Pascal, donde se guardaba un registro detrás del otro; pero con la diferencia de que en este caso no es necesario que todos los registros sean del mismo tipo:


In [3]:
import pickle  # Importo la biblioteca necesaria

# Creo la variable archivo
with open('ejemplo.pkl', 'wb') as archivo:
    pkl = pickle.Pickler(archivo)  # Creo mi punto de acceso a los datos a partir del archivo

    lista1 = [1, 2, 3]
    lista2 = [4, 5]
    diccionario = {'campo1': 1, 'campo2': 'dos'}

    pkl.dump(lista1)         # Guardo la lista1 de [1, 2, 3]
    pkl.dump(None)           # Guardo el valor None
    pkl.dump(lista2)
    pkl.dump('Hola mundo')
    pkl.dump('')
    pkl.dump(diccionario)
    pkl.dump(1)

Para leer de un archivo pickle no puedo usar el método readline que usa la estructura for, por lo que no me queda otra que siempre intentar leer hasta que lance una excepción del tipo EOFError:


In [4]:
import pickle
with open('ejemplo.pkl', 'rb') as archivo:
    print pickle.load(archivo)  # lista1
    print pickle.load(archivo)  # None
    print pickle.load(archivo)  # lista2
    print pickle.load(archivo)  # Hola mundo
    print pickle.load(archivo)  # ''
    print pickle.load(archivo)  # diccionario
    print pickle.load(archivo)  # 1
    print pickle.load(archivo)  # Fin de archivo


[1, 2, 3]
None
[4, 5]
Hola mundo

{'campo1': 1, 'campo2': 'dos'}
1
---------------------------------------------------------------------------
EOFError                                  Traceback (most recent call last)
<ipython-input-4-08b16b682993> in <module>()
      8     print pickle.load(archivo)  # diccionario
      9     print pickle.load(archivo)  # 1
---> 10     print pickle.load(archivo)  # Fin de archivo
     11 

/usr/lib/python2.7/pickle.pyc in load(file)
   1382 
   1383 def load(file):
-> 1384     return Unpickler(file).load()
   1385 
   1386 def loads(str):

/usr/lib/python2.7/pickle.pyc in load(self)
    862             while 1:
    863                 key = read(1)
--> 864                 dispatch[key](self)
    865         except _Stop, stopinst:
    866             return stopinst.value

/usr/lib/python2.7/pickle.pyc in load_eof(self)
    884 
    885     def load_eof(self):
--> 886         raise EOFError
    887     dispatch[''] = load_eof
    888 

EOFError: 

In [7]:
with open('ejemplo.pkl', 'rb') as archivo:
    seguir_leyendo = True
    while seguir_leyendo:
        try:
            dato = pickle.load(archivo)  # Leo del archivo un elemento
            print dato
        except EOFError:
            seguir_leyendo = False


[1, 2, 3]
None
[4, 5]
Hola mundo

{'campo1': 1, 'campo2': 'dos'}
1

Ejemplo 2: Guardo una lista de elementos

Así como en el ejemplo anterior guardamos de a un elemento por vez, también podríamos guardar una lista completa que tenga todos los elementos en memoria. De ésta forma, los archivos podrían usarse para cargar los datos al comenzar el programa y guardarlos todos juntos antes de terminar.
Suponiendo que estoy desarrollando un juego en que no van a haber muchos jugadores compitiendo entre si, podría tener una lista con los puntajes y hacer:


In [5]:
lista = [  # Creo la lista que quiero guardar
    {'usuario': 'csanchez', 'puntaje': 5}, 
    {'usuario': 'pperez', 'puntaje': 3}, 
    {'usuario': 'jromero', 'puntaje': 1}, 
]

# Guardo la lista en el archiv
with open('ejemplo_2.pkl', 'wb') as archivo:
    pkl = pickle.Pickler(archivo)
    pkl.dump(lista)

Y si ahora quiero sumarle 3 puntos a un usuario en particular tendría que:

  1. Leer todo el archivo en una lista
  2. Buscar el usuario y actualizarle los puntos
  3. Guardar toda la lista en el archivo

In [6]:
def imprimir_puntajes(lista_puntajes):
    for puntaje in lista_puntajes:
        print '    ', puntaje


# Leo del archivo
with open('ejemplo_2.pkl', 'rb') as archivo:
    lista_puntajes = pickle.load(archivo)


# Actualizo el puntaje
print 'La lista antes de hacer el cambio es:'
imprimir_puntajes(lista_puntajes)

pos =  0
lista_puntajes[pos]['puntaje'] += 3

print 'La lista una vez hecho el cambio es:'
imprimir_puntajes(lista_puntajes)

# Guardo la lista en el archiv
with open('ejemplo_2.pkl', 'wb') as archivo:
    pkl = pickle.Pickler(archivo)
    pkl.dump(lista_puntajes)


La lista antes de hacer el cambio es:
     {'puntaje': 5, 'usuario': 'csanchez'}
     {'puntaje': 3, 'usuario': 'pperez'}
     {'puntaje': 1, 'usuario': 'jromero'}
La lista una vez hecho el cambio es:
     {'puntaje': 8, 'usuario': 'csanchez'}
     {'puntaje': 3, 'usuario': 'pperez'}
     {'puntaje': 1, 'usuario': 'jromero'}

Si bien es muy práctica esta alternativa, tiene el gran inconveniente de no hacer un uso eficiente de la memoria.
Si el archivo contiene millones de usuarios, los estaríamos levantando todos a memoria, con el gran costo que tiene eso (no sólo en espacio, sino también en tiempo) con el único objetivo de sumarle 3 puntos a un único usuario. Y una vez que actualizamos el puntaje de ese usuario, tendríamos que volver a guardar todo el archivo en el disco.

Abstrayendonos del uso de los pickles

Si bien el uso de los pickles puede resultar muy útil, la forma de leer la información guardada en ellos no suele ser muy cómoda. Por lo que podríamos implementar en un archivo utils.py las siguientes dos funciones para abstraernos un poco de cómo se accede a los datos:

# encoding: utf8
import pickle

def guardar_en_archivo(archivo, contenido):
    """Guarda lo que le pasen como segundo parámetro en el archivo 
    que recibe como primer parámetro.
    El parámetro llamado archivo tiene que estar abieto en modo 
    binario y para escritura (wb)
    """
    pickler = pickle.Pickler(archivo)
    pickler.dump(contenido)


def leer_desde_archivo(archivo, valor_por_defecto=None):
    """Lee del archivo archivo un registro y lo retorna junto con una
    variable booleana que indica si llegó al fin de archivo o no.
    El parámetro llamado archivo tiene que estar abieto en modo 
    binario y para lectura (rb).
    Si se intenta leer más allá del fin de archivo, data valdrá lo que le 
    hayan pasado en valor_por_defecto (si no le pasan nada será None)
    y fin_de_archivo será True. En cualquier otro caso fin_de_archivo
    será False.
    """
    try:
        data = pickle.load(archivo)
        fin_de_archivo = False
    except EOFError:
        data = valor_por_defecto
        fin_de_archivo = True
    return data, fin_de_archivo

En este caso, se podría considerar que la función leer_desde_archivo funciona similar a cómo lo hacen los archivos con un registro centinella.
Por lo que podríamos usar:


In [7]:
import utils


curso = [
    {'nombre': 'Sanchez, Lucas', 'nota': 8, 'padron': 90431, 'grupo': 1},
    {'nombre': 'Alvarez, Javier', 'nota': 2, 'padron': 92953, 'grupo': 1},
    {'nombre': 'Perez, Matias', 'nota': 10, 'padron': 92407, 'grupo': 1},
    {'nombre': 'Lopez, Pablo', 'nota': 9, 'padron': 96556, 'grupo': 2},
    {'nombre': 'Gonzalez, Marcelo', 'nota': 7, 'padron': 92143, 'grupo': 2},
    {'nombre': 'Rodriguez, Pablo', 'nota': 9, 'padron': 92431, 'grupo': 2},
    {'nombre': 'Gomez, Matias', 'nota': 4, 'padron': 98306, 'grupo': 3},
    {'nombre': 'Diaz, Juan', 'nota': 8, 'padron': 97972, 'grupo': 3},
    {'nombre': 'Garcia, Matias', 'nota': 2, 'padron': 93108, 'grupo': 4},
    {'nombre': 'Rodriguez, Agustin', 'nota': 5, 'padron': 96739, 'grupo': 5},
]

print 'Creo el archivo vacío usando el modo "wb"'
print 'Si tenía algo, ya lo borre...'
MAX = {'padron': 999999999999}
with open('curso.pkl', 'wb') as archivo:
    for alumno in curso:
        print 'Guardando el alumno {} en el archivo'.format(alumno)
        utils.guardar_en_archivo(archivo, alumno)

print
print 'Abro el archivo en modo lectura...'
with open('curso.pkl', 'rb') as archivo:
    alumno, fin_de_archivo = utils.leer_desde_archivo(archivo)
    while not fin_de_archivo:
        print 'Leyendo el alumno {} en el archivo'.format(alumno)
        alumno, fin_de_archivo = utils.leer_desde_archivo(archivo)


Creo el archivo vacío usando el modo "wb"
Si tenía algo, ya lo borre...
Guardando el alumno {'nombre': 'Sanchez, Lucas', 'grupo': 1, 'nota': 8, 'padron': 90431} en el archivo
Guardando el alumno {'nombre': 'Alvarez, Javier', 'grupo': 1, 'nota': 2, 'padron': 92953} en el archivo
Guardando el alumno {'nombre': 'Perez, Matias', 'grupo': 1, 'nota': 10, 'padron': 92407} en el archivo
Guardando el alumno {'nombre': 'Lopez, Pablo', 'grupo': 2, 'nota': 9, 'padron': 96556} en el archivo
Guardando el alumno {'nombre': 'Gonzalez, Marcelo', 'grupo': 2, 'nota': 7, 'padron': 92143} en el archivo
Guardando el alumno {'nombre': 'Rodriguez, Pablo', 'grupo': 2, 'nota': 9, 'padron': 92431} en el archivo
Guardando el alumno {'nombre': 'Gomez, Matias', 'grupo': 3, 'nota': 4, 'padron': 98306} en el archivo
Guardando el alumno {'nombre': 'Diaz, Juan', 'grupo': 3, 'nota': 8, 'padron': 97972} en el archivo
Guardando el alumno {'nombre': 'Garcia, Matias', 'grupo': 4, 'nota': 2, 'padron': 93108} en el archivo
Guardando el alumno {'nombre': 'Rodriguez, Agustin', 'grupo': 5, 'nota': 5, 'padron': 96739} en el archivo

Abro el archivo en modo lectura...
Leyendo el alumno {'nombre': 'Sanchez, Lucas', 'grupo': 1, 'nota': 8, 'padron': 90431} en el archivo
Leyendo el alumno {'nombre': 'Alvarez, Javier', 'grupo': 1, 'nota': 2, 'padron': 92953} en el archivo
Leyendo el alumno {'nombre': 'Perez, Matias', 'grupo': 1, 'nota': 10, 'padron': 92407} en el archivo
Leyendo el alumno {'nombre': 'Lopez, Pablo', 'grupo': 2, 'nota': 9, 'padron': 96556} en el archivo
Leyendo el alumno {'nombre': 'Gonzalez, Marcelo', 'grupo': 2, 'nota': 7, 'padron': 92143} en el archivo
Leyendo el alumno {'nombre': 'Rodriguez, Pablo', 'grupo': 2, 'nota': 9, 'padron': 92431} en el archivo
Leyendo el alumno {'nombre': 'Gomez, Matias', 'grupo': 3, 'nota': 4, 'padron': 98306} en el archivo
Leyendo el alumno {'nombre': 'Diaz, Juan', 'grupo': 3, 'nota': 8, 'padron': 97972} en el archivo
Leyendo el alumno {'nombre': 'Garcia, Matias', 'grupo': 4, 'nota': 2, 'padron': 93108} en el archivo
Leyendo el alumno {'nombre': 'Rodriguez, Agustin', 'grupo': 5, 'nota': 5, 'padron': 96739} en el archivo

Cortes de control

Cuando estamos hablando de archivos y nos referimos a corte de control estamos haciendo referencia al algoritmo que toma un archivo ordenado, por una o más claves, y como resultado del mismo nos devuelve un "resumen" del mismo.
Por ejemplo, si tenemos el siguiente archivo ordenado por código de cliente:

Código de cliente Número de factura Monto de la factura
001 2020452 916
002 12069115 772
002 14534467 264
002 1424980 752
002 16214863 424
003 6882583 590
003 18817277 654
003 1944327 211
003 16837776 595
003 10145610 444
004 4671025 393
004 13453769 556
005 7126081 35
005 16497082 367

Y queremos calcular cuánta plata gastó cada cliente en nuestro negocio:

El cliente 001 gastó  916
El cliente 002 gastó 2212
El cliente 003 gastó 2494
El cliente 004 gastó  949
El cliente 005 gastó  402

Podríamos usar el siguiente algoritmo para generar el reporte:


In [8]:
import utils


def crear_archivo_de_ventas():
    ventas = [
        {'cliente': '001', 'nro_factura': 2020452, 'monto': 916},
        {'cliente': '002', 'nro_factura': 12069115, 'monto': 772},
        {'cliente': '002', 'nro_factura': 14534467, 'monto': 264},
        {'cliente': '002', 'nro_factura': 1424980, 'monto': 752},
        {'cliente': '002', 'nro_factura': 16214863, 'monto': 424},
        {'cliente': '003', 'nro_factura': 6882583, 'monto': 590},
        {'cliente': '003', 'nro_factura': 18817277, 'monto': 654},
        {'cliente': '003', 'nro_factura': 1944327, 'monto': 211},
        {'cliente': '003', 'nro_factura': 16837776, 'monto': 595},
        {'cliente': '003', 'nro_factura': 10145610, 'monto': 444},
        {'cliente': '004', 'nro_factura': 4671025, 'monto': 393},
        {'cliente': '004', 'nro_factura': 13453769, 'monto': 556},
        {'cliente': '005', 'nro_factura': 7126081, 'monto': 35},
        {'cliente': '005', 'nro_factura': 16497082, 'monto': 367}
    ]

    print 'Creo el archivo vacío usando el modo "wb"'
    with open('ventas.pkl', 'wb') as archivo:
        for venta in ventas:
            utils.guardar_en_archivo(archivo, venta)
            
            
def mostrar_ventas_por_cliente(archivo):
    valor_por_defecto = {'cliente': None, 'monto':0}
    total = 0
    # Leo el primer registro del archivo
    venta, fin_de_archivo = utils.leer_desde_archivo(archivo, valor_por_defecto)
    codigo_cliente = venta['cliente']
    while not fin_de_archivo:
        codigo_cliente = venta['cliente']
        subtotal = 0  # Inicializo las ventas de este cliente
        # Mientras siga procesando el mismo cliente...
        while venta['cliente'] == codigo_cliente:
            total += venta['monto']  # Acumulo las ventas totales
            subtotal += venta['monto']  # Acumulo las ventas de este cliente
            venta, fin_de_archivo = utils.leer_desde_archivo(archivo, valor_por_defecto)

        print '      El cliente {cliente} gastó {monto:4}'.format(cliente=codigo_cliente, monto=subtotal)

    print 'El total es de ${}'.format(total)
    

crear_archivo_de_ventas()
print 'Abro el archivo en modo lectura...'

# Abro el archivo usando el with para asegurarme 
# que, pase lo que pase, el archivo quede cerrado
with open('ventas.pkl', 'rb') as archivo:
    mostrar_ventas_por_cliente(archivo)


Creo el archivo vacío usando el modo "wb"
Abro el archivo en modo lectura...
      El cliente 001 gastó  916
      El cliente 002 gastó 2212
      El cliente 003 gastó 2494
      El cliente 004 gastó  949
      El cliente 005 gastó  402
El total es de $6973

¿Y si el archivo fuera de texto?
Es simple, tratamos de llevar el problema a la solución que conocemos. Para eso podríamos crearnos una función que se comporte de una forma similar a la que se encuentra en utils:

def leer_desde_archivo(archivo, valor_por_defecto):
    try:
        linea = archivo.readline()
        codigo_cliente, factura, monto = linea.strip().split(',')
        data = {
            'cliente': codigo_cliente,
            'factura': int(factura),
            'monto': int(monto)
        }
        fin_de_archivo = False
    except StopIteration:
        data = valor_por_defecto
        fin_de_archivo = True

    return data, fin_de_archivo

Esta función, no sólo lee cada línea, sino que una vez leída:

  1. usa de la función strip para quitar el \n que tiene al final de la línea
  2. usa la función split para separar el string por comas
  3. hace uso del unpacking para guardar en 3 variables distintas cada uno de los campos de la línea
  4. crea un diccionario con cada uno de los datos que obtuvo de la línea, pero antes, convierte el número de factura y el monto a entero usando la función int

Por último, si ya habíamos llegado al final del archivo e intentamos leer de nuevo, el intérprete va a lanzar la excepción StopIteration que la capturamos con el try-except y, en ese caso, devolvemos el valor que nos pasaron por parámetro.

Entonces, después el algoritmo nos queda igual, a excepción de que ahora no importamos a la utils y la forma de crear y abrir el archivo va a ser distinta:


In [9]:
def leer_desde_archivo(archivo, valor_por_defecto):
    try:
        linea = archivo.readline()
        codigo_cliente, factura, monto = linea.strip().split(',')
        data = {
            'cliente': codigo_cliente,
            'factura': int(factura),
            'monto': int(monto)
        }
        fin_de_archivo = False
    except Exception:
        data = valor_por_defecto
        fin_de_archivo = True
    
    return data, fin_de_archivo


def crear_archivo_de_ventas():
    ventas = """001,2020452,916
002,12069115,772
002,14534467,264
002,1424980,752
002,16214863,424
003,6882583,590
003,18817277,654
003,1944327,211
003,16837776,595
003,10145610,444
004,4671025,393
004,13453769,556
005,7126081,35
005,16497082,367
"""
    print 'Creo el archivo vacío usando el modo "wt"'
    with open('ventas.csv', 'wt') as archivo:
        archivo.write(ventas)
            
            
def mostrar_ventas_por_cliente(archivo):
    valor_por_defecto = {'cliente': None, 'monto':0}
    total = 0
    # Leo el primer registro del archivo
    venta, fin_de_archivo = leer_desde_archivo(archivo, valor_por_defecto)
    codigo_cliente = venta['cliente']
    while not fin_de_archivo:
        codigo_cliente = venta['cliente']
        subtotal = 0  # Inicializo las ventas de este cliente
        # Mientras siga procesando el mismo cliente...
        while venta['cliente'] == codigo_cliente:
            total += venta['monto']  # Acumulo las ventas totales
            subtotal += venta['monto']  # Acumulo las ventas de este cliente
            venta, fin_de_archivo = leer_desde_archivo(archivo, valor_por_defecto)

        print '      El cliente {cliente} gastó {monto:4}'.format(cliente=codigo_cliente, monto=subtotal)

    print 'El total es de ${}'.format(total)
    

crear_archivo_de_ventas()
print 'Abro el archivo en modo lectura...'
# Abro el archivo usando el with para asegurarme 
# que, pase lo que pase, el archivo quede cerrado
with open('ventas.csv', 'rt') as archivo:
    mostrar_ventas_por_cliente(archivo)


Creo el archivo vacío usando el modo "wt"
Abro el archivo en modo lectura...
      El cliente 001 gastó  916
      El cliente 002 gastó 2212
      El cliente 003 gastó 2494
      El cliente 004 gastó  949
      El cliente 005 gastó  402
El total es de $6973

Merge de archivos

El merge, o apareo, de archivos consiste en tener dos o más archivos que se encuentran ordenados por una misma clave y se quieren procesar leyéndolos una única vez y generar un reporte o un nuevo archivo con dicha información consolidada. Supongamos que tenemos el siguiente código para crear unos archivos de prueba:


In [11]:
import utils
oper = [
    {'cta': 1, 'imp': 800},
    {'cta': 1, 'imp': 250},
    {'cta': 2, 'imp': 700},
    {'cta': 2, 'imp': 700},
    {'cta': 10, 'imp': 1000},
]
with open('movs1.pkl', 'wb') as archivo:
    for movimiento in oper:
        print 'Guardando la operacion {} en el archivo movs1.pkl'.format(movimiento)
        utils.guardar_en_archivo(archivo, movimiento)

print

operaciones_2 = [
    {'cta': 1, 'imp': 800},
    {'cta': 2, 'imp': 700},
    {'cta': 3, 'imp': 700},
    {'cta': 10, 'imp': 100},
    {'cta': 15, 'imp': 3},
]
with open('movs2.pkl', 'wb') as archivo:
    for movimiento in operaciones_2:
        print 'Guardando la operacion {} en el archivo movs2.pkl'.format(movimiento)
        utils.guardar_en_archivo(archivo, movimiento)


Guardando la operacion {'imp': 800, 'cta': 1} en el archivo movs1.pkl
Guardando la operacion {'imp': 250, 'cta': 1} en el archivo movs1.pkl
Guardando la operacion {'imp': 700, 'cta': 2} en el archivo movs1.pkl
Guardando la operacion {'imp': 700, 'cta': 2} en el archivo movs1.pkl
Guardando la operacion {'imp': 1000, 'cta': 10} en el archivo movs1.pkl

Guardando la operacion {'imp': 800, 'cta': 1} en el archivo movs2.pkl
Guardando la operacion {'imp': 700, 'cta': 2} en el archivo movs2.pkl
Guardando la operacion {'imp': 700, 'cta': 3} en el archivo movs2.pkl
Guardando la operacion {'imp': 100, 'cta': 10} en el archivo movs2.pkl
Guardando la operacion {'imp': 3, 'cta': 15} en el archivo movs2.pkl

Entonces si lo que quiero es mostrar el estado de cada una de estas cuentas podría hacer:


In [12]:
import utils

with open('movs1.pkl', 'rb')as movs1, open('movs2.pkl', 'rb') as movs2:
    MAX = {'cta': 99999999, 'imp':0}
    oper1, eof1 = utils.leer_desde_archivo(movs1, MAX)
    oper2, eof2 = utils.leer_desde_archivo(movs2, MAX)
    total = 0
    while not eof1 or not eof2:
         totcta = 0
         men = min(oper1['cta'], oper2['cta'])
         print 'La menor de las cuentas entre {} y {} es {}'.format(
            oper1['cta'],
            oper2['cta'],
            men
        )
         while oper1['cta'] == men:
                    print 'Procesando la cuenta {} de movs1'.format(oper1['cta'])
                    total += oper1['imp']
                    totcta += oper1['imp']
                    oper1, eof1 = utils.leer_desde_archivo(movs1, MAX)

         while  oper2['cta'] == men:
                    print 'Procesando la cuenta {} de movs2'.format(oper2['cta'])
                    total += oper2['imp']
                    totcta += oper2['imp']
                    oper2, eof2 = utils.leer_desde_archivo(movs2, MAX)

         print 'Total por cta:', men , totcta

print 'Total Gral:', total


La menor de las cuentas entre 1 y 1 es 1
Procesando la cuenta 1 de movs1
Procesando la cuenta 1 de movs1
Procesando la cuenta 1 de movs2
Total por cta: 1 1850
La menor de las cuentas entre 2 y 2 es 2
Procesando la cuenta 2 de movs1
Procesando la cuenta 2 de movs1
Procesando la cuenta 2 de movs2
Total por cta: 2 2100
La menor de las cuentas entre 10 y 3 es 3
Procesando la cuenta 3 de movs2
Total por cta: 3 700
La menor de las cuentas entre 10 y 10 es 10
Procesando la cuenta 10 de movs1
Procesando la cuenta 10 de movs2
Total por cta: 10 1100
La menor de las cuentas entre 99999999 y 15 es 15
Procesando la cuenta 15 de movs2
Total por cta: 15 3
Total Gral: 5753

Actualizar archivo con novedades

Cuando tiene toda la información en un único archivo (comúnmente llamado archivo maestro) y en cierto momento se la quiere actualizar a partir de un segundo archivo llamado novedades se genera un tercer archivo con toda la información consolidada.
Los archivos maestro y novedades deberán estar ordenados por la misma clave, por lo que el nuevo archivo maestro también debe quedar ordenado. Por ejemplo, si contamos con un archivo llamado cuentas.pkl que en casa posición tiene la información correspondiente a una cuenta bancaria:

  • nro_cuenta: Número de cuenta
  • tituar: Titular de la cuenta
  • saldo: Saldo de la cuenta
  • tipo_cuenta: Tipo de cuenta
  • moneda: Moneda en la cual opera la cuenta

Y uno que tenga las novedades diarias llamado movimientos.pkl con la siguiente información:

  • tipo: Tipo de movimiento, es un string de una letra que puede ser A (alta), B (baja), M (modificación)
  • nro_cuenta
  • Si es:
    • alta(se asume saldo 0):
      • titular
      • tipo_cuenta
      • moneda
    • modificación:
      • tipo_movimiento: Un string que será una de las siguientes opciones: "credito" (cuando ingresa plata a la cuenta) o "debito" (cuando extraen plata de la cuenta)
      • monto: Monto a acreditar o debitar de la cuenta
    • baja: no es necesario agregar más campos

In [13]:
import utils


def crear_archivo_maestro():
    cuentas = [
        {'nro_cuenta': 1, 'saldo': 7094, 'moneda': '$', 
         'tipo_cuenta': 'debito', 'titular': 'cliente_1'}, 
        {'nro_cuenta': 2, 'saldo': 2896, 'moneda': '$', 
         'tipo_cuenta': 'debito', 'titular': 'cliente_2'}, 
        {'nro_cuenta': 3, 'saldo': 14424, 'moneda': '$', 
         'tipo_cuenta': 'corriente', 'titular': 'cliente_3'}, 
        {'nro_cuenta': 5, 'saldo': 7156, 'moneda': '$', 
         'tipo_cuenta': 'corriente', 'titular': 'cliente_5'}, 
        {'nro_cuenta': 8, 'saldo': 7500, 'moneda': '$', 
         'tipo_cuenta': 'corriente', 'titular': 'cliente_8'}, 
        {'nro_cuenta': 9, 'saldo': 2128, 'moneda': '$', 
         'tipo_cuenta': 'debito', 'titular': 'cliente_9'}, 
        {'nro_cuenta': 13, 'saldo': 13524, 'moneda': '$', 
         'tipo_cuenta': 'corriente', 'titular': 'cliente_13'}, 
        {'nro_cuenta': 15, 'saldo': 9479, 'moneda': '$', 
         'tipo_cuenta': 'debito', 'titular': 'cliente_15'}, 
        {'nro_cuenta': 21, 'saldo': 8462, 'moneda': '$', 
         'tipo_cuenta': 'debito', 'titular': 'cliente_21'}, 
        {'nro_cuenta': 25, 'saldo': 6258, 'moneda': '$', 
         'tipo_cuenta': 'debito', 'titular': 'cliente_25'}, 
        {'nro_cuenta': 32, 'saldo': 14082, 'moneda': '$', 
         'tipo_cuenta': 'debito', 'titular': 'cliente_32'}
    ]
    with open('cuentas.pkl', 'wb') as archivo:
        for cuenta in cuentas:
            utils.guardar_en_archivo(archivo, cuenta)


def crear_archivo_novedades():
    novedades = [
        {'nro_cuenta': 1, 'tipo': 'B'},
        {'nro_cuenta': 2, 'monto': 731, 
         'tipo_movimiento': 'debito', 'tipo': 'M'},
        {'nro_cuenta': 3, 'monto': 791, 
         'tipo_movimiento': 'debito', 'tipo': 'M'},
        {'nro_cuenta': 4, 'moneda': '$', 
         'tipo_cuenta': 'corriente', 
         'titular': 'cliente_4', 'tipo': 'A'},
        {'nro_cuenta': 8, 'monto': 750, 
         'tipo_movimiento': 'debito', 'tipo': 'M'},
        {'nro_cuenta': 11, 'moneda': '$', 
         'tipo_cuenta': 'corriente', 'titular': 'cliente_11', 
         'tipo': 'A'},
        {'nro_cuenta': 13, 'monto': 481, 
         'tipo_movimiento': 'debito', 'tipo': 'M'},
        {'nro_cuenta': 15, 'tipo': 'B'},
        {'nro_cuenta': 16, 'moneda': '$', 
         'tipo_cuenta': 'debito', 'titular': 'cliente_16', 
         'tipo': 'A'},
        {'nro_cuenta': 19, 'moneda': '$', 
         'tipo_cuenta': 'corriente', 'titular': 'cliente_19', 
         'tipo': 'A'},
        {'nro_cuenta': 21, 'monto': 653, 
         'tipo_movimiento': 'debito', 'tipo': 'M'},
        {'nro_cuenta': 25, 'tipo': 'B'},
        {'nro_cuenta': 32, 'tipo': 'B'},
    ]
    with open('movimientos.pkl', 'wb') as archivo:
        for nov in novedades:
            utils.guardar_en_archivo(archivo, nov)


################### Apareo ###################

def dar_de_alta(archivo, novedad):
    del novedad['tipo']  # Le borro el campo tipo que no
                         # existe en el archivo maestro
    novedad['saldo'] = 0  # Inicializo el saldo en 0
    utils.guardar_en_archivo(archivo, novedad)

    
def modificar_cuenta(archivo, cuenta, novedad):
    if novedad['tipo_movimiento'] == 'credito':
        monto = novedad['monto']
    else:
        monto = -1 * novedad['monto']

    cuenta['saldo'] += monto
    utils.guardar_en_archivo(archivo, cuenta)
    
def apareo(maestro, novedades, nuevo):
    cuenta, eof_ctas = utils.leer_desde_archivo(maestro)
    nov, eof_novs = utils.leer_desde_archivo(novedades)
    while not eof_ctas and not eof_novs:
        print 'Procesando cuenta nro {} y novedad {} del tipo {}'.format(
            cuenta['nro_cuenta'], nov['nro_cuenta'], nov['tipo']
        )
        if nov['nro_cuenta'] < cuenta['nro_cuenta'] and nov['tipo'] == 'A':
            # Si es un alta, acomodo el registro y lo guardo
            dar_de_alta(nuevo, nov)
            nov, eof_novs = utils.leer_desde_archivo(novedades)

            # No puede ser una B o M porque habría un error
        elif nov['nro_cuenta'] == cuenta['nro_cuenta']:
            if nov['tipo'] == 'M':
                # Si es una modificación, actualizo la cuenta, 
                # guardo y leo de los dos archivos
                modificar_cuenta(nuevo, cuenta, nov)

            cuenta, eof_ctas = utils.leer_desde_archivo(maestro)
            nov, eof_novs = utils.leer_desde_archivo(novedades)

            # Si fuera una B, tendría que ignorarlos y leer de 
            # los archivos igual.

            # No puede ser una A porque habría un error
        elif nov['nro_cuenta'] > cuenta['nro_cuenta']:
            # Si la novedad tiene un número de cuenta mayor, 
            # significa que para esa cuenta no hubo novedades
            # por lo que la guardo tal cual esta sin modificar
            # y leo la siguiente
            utils.guardar_en_archivo(nuevo, cuenta)
            cuenta, eof_ctas = utils.leer_desde_archivo(maestro)

    # Como salí del while, termine con al menos uno de los
    # dos archivos, por lo que ahora puedeo leer lo que
    # quedaba y guardarlo casi tal cual vienen
    while not eof_ctas:
        print 'Procesando cuenta nro {}'.format(cuenta['nro_cuenta'])
        utils.guardar_en_archivo(nuevo, cuenta)
        cuenta, eof_ctas = utils.leer_desde_archivo(maestro)

    while not eof_novs:
        print 'Procesando la novedad {} del tipo {}'.format(
            nov['nro_cuenta'], nov['tipo']
        )
        del nov['tipo']
        nov['saldo'] = 0
        utils.guardar_en_archivo(nuevo, nov)
        nov, eof_novs = utils.leer_desde_archivo(novedades)


def mostrar_archivo_nuevo():
    print 'El archivo nuevo tiene los registros:'
    with open('nuevo.pkl', 'rb') as nuevo:
        cuenta, eof_ctas = utils.leer_desde_archivo(nuevo)
        while not eof_ctas:
            print cuenta
            cuenta, eof_ctas = utils.leer_desde_archivo(nuevo)            


crear_archivo_maestro()
crear_archivo_novedades()

with open('cuentas.pkl', 'rb') as maestro, \
    open('movimientos.pkl', 'rb') as novedades, \
    open('nuevo.pkl', 'wb') as nuevo:
        apareo(maestro, novedades, nuevo)

mostrar_archivo_nuevo()


Procesando cuenta nro 1 y novedad 1 del tipo B
Procesando cuenta nro 2 y novedad 2 del tipo M
Procesando cuenta nro 3 y novedad 3 del tipo M
Procesando cuenta nro 5 y novedad 4 del tipo A
Procesando cuenta nro 5 y novedad 8 del tipo M
Procesando cuenta nro 8 y novedad 8 del tipo M
Procesando cuenta nro 9 y novedad 11 del tipo A
Procesando cuenta nro 13 y novedad 11 del tipo A
Procesando cuenta nro 13 y novedad 13 del tipo M
Procesando cuenta nro 15 y novedad 15 del tipo B
Procesando cuenta nro 21 y novedad 16 del tipo A
Procesando cuenta nro 21 y novedad 19 del tipo A
Procesando cuenta nro 21 y novedad 21 del tipo M
Procesando cuenta nro 25 y novedad 25 del tipo B
Procesando cuenta nro 32 y novedad 32 del tipo B
El archivo nuevo tiene los registros:
{'nro_cuenta': 2, 'saldo': 2165, 'moneda': '$', 'titular': 'cliente_2', 'tipo_cuenta': 'debito'}
{'nro_cuenta': 3, 'saldo': 13633, 'moneda': '$', 'titular': 'cliente_3', 'tipo_cuenta': 'corriente'}
{'nro_cuenta': 4, 'saldo': 0, 'moneda': '$', 'tipo_cuenta': 'corriente', 'titular': 'cliente_4'}
{'nro_cuenta': 5, 'saldo': 7156, 'moneda': '$', 'titular': 'cliente_5', 'tipo_cuenta': 'corriente'}
{'nro_cuenta': 8, 'saldo': 6750, 'moneda': '$', 'titular': 'cliente_8', 'tipo_cuenta': 'corriente'}
{'nro_cuenta': 9, 'saldo': 2128, 'moneda': '$', 'titular': 'cliente_9', 'tipo_cuenta': 'debito'}
{'nro_cuenta': 11, 'saldo': 0, 'moneda': '$', 'tipo_cuenta': 'corriente', 'titular': 'cliente_11'}
{'nro_cuenta': 13, 'saldo': 13043, 'moneda': '$', 'titular': 'cliente_13', 'tipo_cuenta': 'corriente'}
{'nro_cuenta': 16, 'saldo': 0, 'moneda': '$', 'tipo_cuenta': 'debito', 'titular': 'cliente_16'}
{'nro_cuenta': 19, 'saldo': 0, 'moneda': '$', 'tipo_cuenta': 'corriente', 'titular': 'cliente_19'}
{'nro_cuenta': 21, 'saldo': 7809, 'moneda': '$', 'titular': 'cliente_21', 'tipo_cuenta': 'debito'}

Ejercicios

  1. Hacer el corte de control para un archivo que tenga la información del curso de algoritmos 1 y nos diga si todos sus integrantes aprobaron el parcial y el promedio de sus notas.
  2. Hacer el apareo, pero asumiendo que pueden venir más de un movimiento por cuenta (puede ser un alta, varios debitos/creditos e incluso una baja).
  3. Hacer un merge de dos archivos ordenados, que no es más que mezclar dos archivos del mismo tipo (por ejemplo, dos archivos maestro de cuentas bancarias) y generar un tercero donde se encuentren todos los registros de los primeros dos.
  4. Suponiendo que existe un archivo llamado utils.py donde se encuentran las funciones:
def guardar_en_archivo(archivo, contenido):
    """Guarda lo que le pasen como segundo parámetro en el archivo 
    que recibe como primer parámetro.
    El parámetro llamado archivo tiene que estar abieto en modo 
    binario y para escritura (wb)
    """
    ...


def leer_desde_archivo(archivo, valor_por_defecto=None):
    """Lee del archivo archivo un registro y lo retorna junto con una
    variable booleana que indica si llegó al fin de archivo o no.
    El parámetro llamado archivo tiene que estar abieto en modo 
    binario y para lectura (rb).
    Si se intenta leer más allá del fin de archivo, data valdrá lo que le 
    hayan pasado en valor_por_defecto (si no le pasan nada será None)
    y fin_de_archivo será True. En cualquier otro caso fin_de_archivo
    será False.
    """
    ...
    return data, fin_de_archivo

Leer dos archivos (61_matematica.dat y 75_computacion.dat) que tendrán registros con los campos:

* padron
* nombre
* apellido
* nota
* codigo_departamento
* codigo_materia

y armar uno nuevo donde sólo figuren las notas de los alumnos aprobados ordenados por padrón.
Ambos archivos están ordenados por padrón y se deben leer una única vez. Como los archivos pueden ser muy grandes, no se pueden guardar en memoria.
Una vez procesados los dos archivos se tienen que informar, para cada materia, cuántos alumnos aprobaron y cuántos desaprobaron.